package com.example.springframeworklearning.aspect;

import com.example.springframeworklearning.aspect.annotation.RefreshCache;
import com.example.springframeworklearning.domain.Order;
import lombok.extern.slf4j.Slf4j;
import org.aspectj.lang.JoinPoint;
import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.annotation.*;
import org.springframework.core.Ordered;
import org.springframework.stereotype.Component;

import java.time.LocalDateTime;
import java.util.ArrayList;
import java.util.List;
import java.util.Set;

/**
 * 将 @Aspect 注解标记的类 以 bean 的形式注册到 ioc 中
 * spring 就能自动的检测到 Aspect
 * https://docs.spring.io/spring-framework/docs/current/reference/html/core.html#aop-pointcuts
 * spring aop 是基于代理系统来实现的 aspect
 * 默认使用jdk动态代理，前提是被代理对象实现了接口的话。
 * ⚠️ 如果实现类中的方法没有在接口中声明，那么jdk动态代理是不会生效的
 * 否则使用CGLid代理
 * ⚠️ CGlib 可以为 protect 和 包可见 的方法 实现代理，但是不能为 private 方法创建代理
 * 因为 private 方法 隐藏了 final 关键字，而 final 方法不能被覆盖，也就无法实现代理功能
 * 约定俗成：一般 与 代理交互的方法 都 应该被设置为 public 修饰符
 *
 * @author luo
 * @since 2022/9/22 10:27
 */
@Component
@Aspect
@Slf4j
public class OrderAspect implements Ordered {

    /**
     * https://docs.spring.io/spring-framework/docs/current/reference/html/core.html#aop-pointcuts
     * 切点的签名由方法的定义来提供
     * 用 @PointCut 注解 指明切点表达式
     * 这个切点的名称就是方法名：即 listOrderPointCut
     * spring 支持的 切点代号：Designators
     * 1、execution：匹配方法执行的 join points
     * 方法名匹配支持通配符（*）
     * () 匹配没有参数的方法
     * (..) 匹配0个或多个参数
     * (*) 匹配一个参数，这个参数可以是任意类型
     * (*,String) 匹配两个参数，第一个个参数是任意类型
     * 2、within：在具体的类型（types）中匹配 join points
     * 3、this：匹配当 spring aop 代理的 bean 是指定的类型。例如：this(com.example.springframeworklearning.service.OrderService)，
     * spring 使用jdk代理实现了此接口，此代理实现委派原来的 OrderServiceImpl 来真正的执行具体的操作，此处指代 jdk 动态代理生成的无法被看到的 OrderService 的一个实现
     * 4、target：(被代理的对象 application object being proxied) 是给定类型的一个实例。例如：target(com.example.springframeworklearning.service.OrderService)，
     * spring 使用jdk代理实现了此接口，此处指代 被代理的对象（OrderServiceImpl）
     * 在 AspectJ 中 this 和 target 指向的都是统一个对象，即执行方法的对象
     * 但是在 基于代理系统实现的 spring aop中，this 和 target 的区别 如上述所示
     * 5、args：join points 中的方法含有 指定类型的参数，举例：args(com.example.springframeworklearning.domain.Order)
     * 6、@target：(被代理的对象 application object being proxied) 是给定 class 的一个实例
     * 7、@args
     * 8、@within
     * 9、@annotation：切点主体（spring aop 中被执行的方法）有指定的的注解 才会被视作 join point
     * 10、bean()：spring 提供了额外了 选择哪一个 beanName 对应 bean 来选择切点
     */
    @Pointcut("execution(java.util.List com.example.springframeworklearning.service.impl.OrderServiceImpl.list*(..))&& bean(orderServiceImpl)")
    public void listOrderPointCut() {
    }


    /**
     * 在指定包下面
     */
    @Pointcut("within(com.example.springframeworklearning.service.impl..*)")
    public void inServicePackage() {
    }


    /**
     * 组合前面两个切点
     */
    @Pointcut("listOrderPointCut()&&inServicePackage()")
    public void combine() {
    }


    /**
     * 只有方法中的参数的类型和 join point 中的类型一致或更高的时候，
     * 此 advice 才会 和 对应的 join point 匹配
     * 不一致的时候也不会抛出异常，只是没有匹配上
     *
     * @param orderList
     */
    @AfterReturning(pointcut = "combine()", returning = "orderList")
    public void beforeOrder(List<Order> orderList) {
        log.info("此次查询到的订单列表是：{}", orderList);
    }

    /**
     * 创建订单方法的切点
     */
    @Pointcut("execution(* com.example.springframeworklearning.service.OrderService.createOrder(*,*))")
    public void createOrderPointCut() {

    }

    /**
     * 创建订单的通知
     *
     * @param proceedingJoinPoint 第一个参数必须为 ProceedingJoinPoint
     * @return 返回值类型必须为 Object
     */
    @Around(value = "createOrderPointCut()&&args(name,createTime)", argNames = "proceedingJoinPoint,name,createTime")
    public Object createOrderAdvice(ProceedingJoinPoint proceedingJoinPoint, String name, LocalDateTime createTime) throws Throwable {

        log.info("Around 我应该是第一个执行");
//        调用无参 的 proceed() 方法不会对 join point 方法的参数做任何处理，而是原样传递参数
        Order proceed = (Order) proceedingJoinPoint.proceed();


        proceed.setName("环绕通知中修改了你的名字");


        return proceed;
    }


    /**
     * args 在切点中指定参数
     * args 的作用
     * 1、限定 join point 方法只能有唯一的一个参数，并且这个参数是 Long 的一个实例
     * 2、使得 advice 方法的 (Long orderId)有效，并能够使用
     *
     * @param orderId
     */
    @Pointcut("execution(* com.example.springframeworklearning.service.impl.OrderServiceImpl.deleteOrder(Long))&&args(orderId)")
    public void deleteOrderPointCut(Long orderId) {

    }


    /**
     * 可以在 advice 方法中使用 pointCut 中指定的参数
     *
     * @param orderId
     */
    @Before(value = "deleteOrderPointCut(orderId)", argNames = "orderId")
    public void deleteOrderViewer(Long orderId) {
        log.info("本次删除的订单id是：{}", orderId);
    }


    /**
     * 在 advice 中拿到 join point 中的注解
     * https://docs.spring.io/spring-framework/docs/current/reference/html/core.html#aop-ataspectj-advice-params
     * 注意 advice 注解中的 argNames 是用于：因为 java 的反射不支持 参数名
     * @param joinPoint
     * @param refreshCache
     * @param current
     * @param size
     */
    @AfterReturning(value = "@annotation(refreshCache)&&args(current,size)", argNames = "joinPoint,refreshCache,current,size")
    public void orderPageCacheAdvice(JoinPoint joinPoint, RefreshCache refreshCache, int current, int size) {
        log.info("本此查询从第{}条记录开始，缓存策略是：{}，从joinPoint 拿出的参数是：{}", current, refreshCache.policy(), joinPoint.getArgs());
    }


    /**
     * 但是不支持集合中使用 class 级别的范型，如果真的有这个需求的话，请使用 List<?> 来 代替 List<T>，再进行手动转换
     * 使用范型
     *
     * @param orderList
     */
    @Before(value = "execution(* com.example.springframeworklearning.service.impl.OrderServiceImpl.createBatchOrder(*))&&args(orderList)", argNames = "orderList")
    public void genericMatch(List<Order> orderList) {
        log.info("advice 支持 方法级别的范型：{}", orderList);
    }

    /**
     * https://docs.spring.io/spring-framework/docs/current/reference/html/core.html#aop-ataspectj-advice-ordering
     * 如果有多个 advice 绑定到了同一个 join point 上
     * 那么 getOrder() 最低的那个有最高的优先级
     * on the way in 最高优先级的最先执行
     * on the way out 最高优先级的最后执行
     * 在 spring framework 5.2.7 及其以上。
     * 同一个 @Aspect 注解标注的 class 下面的多个 advice 绑定 了同一个 join point 那么他们的优先级是
     * @Around @Before, @After, @AfterReturning, @AfterThrowing
     * 但是：同一个 @Aspect 中的 @AfterReturning, @AfterThrowing 会调用 @After 注解，
     * 所以：如果 @After, @AfterReturning, @AfterThrowing 都存在，那么 他们的实际顺序是： @AfterReturning, @AfterThrowing, @After
     * @return
     */
    @Override
    public int getOrder() {
        return 0;
    }



    @After("createOrderPointCut()")
    public void after(){
        log.info("After 我应该是第二个执行");
    }

    @AfterReturning("createOrderPointCut()")
    public void afterReturning(){
        log.info("AfterReturning 我应该是第三个执行，但是我会调用 @After ");
    }
}
